其他
深入窥探动态链接
>>>> 动态链接
动态链接
>>>> 两个表
两个表
1. PLT表(Procedure Linkage Table)
(2)结构简介:
PLT[0] --> 与每个函数第一次链接相关指令
例:
0x4004c0:
0x4004c0: ff 35 42 0b 20 00 push QWORD PTR [rip+0x200b42] // push [GOT[1]]
0x4004c6: ff 25 44 0b 20 00 jmp QWORD PTR [rip+0x200b44] // jmp [GOT[2]]
0x4004cc: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
即:
第一条指令为 push 一个值,该值为 GOT[1] 处存放的地址,
第二条指令为 jmp 到一个地址执行,该值为 GOT[2] 处存放的地址
PLT[1] --> 某个函数链接时所需要的指令,与 got 表一一对应
例:
0x4004d0 <__stack_chk_fail@plt>:
0x4004d0: ff 25 42 0b 20 00 jmp QWORD PTR [rip+0x200b42] // jmp GOT[3]
0x4004d6: 68 00 00 00 00 push 0x0 // push reloc_arg
0x4004db: e9 e0 ff ff ff jmp 0x4004c0 <_init+0x20> // jmp PLT[0]
即:
第一条指令为: jmp 到一个地址执行,该地址为对应 GOT 表项处存放的地址,在下文中会具体讨论这种结构
第二条指令为: push 一个值,该值作用在下文提到
第三个指令为: jmp 一个地址执行,其实该地址就是上边提到的 PLT[0] 的地址,
也就是说接下来要执行 PLT[0] 中保存的两条指令
.
.
.
2. GOT表(Global Offset Table)
(2)结构简介:
GOT[0] --> 此处存放的是 .dynamic 的地址;该节(段)的作用会在下文讨论
GOT[1] --> 此处存放的是 link_map 的地址;该结构也会在下文讨论
GOT[2] --> 此处存放的是 dl_runtime_resolve 函数的地址
GOT[3] --> 与 PLT[1] 对应,存放的是与该表项 (PLT[1]) 要解析的函数相关地址,
由于延迟绑定的原因,开始未调用对应函数时该项存的是 PLT[1] 中第二条指令的地址,
当进行完一次延迟绑定之后存放的才是所要解析的函数的真实地址
GOT[4] --> 与 PLT[2] 对应,所以存放的是与 PLT[2] 所解析的函数相关的地址
.
.
.
3. 两个表之间的关系
GOT[0]: .dynamic 地址 PLT[0]: 与每个函数第一次链接相关指令
GOT[1]: link_map 地址
GOT[2]: dl_runtime_resolve 函数地址
GOT[3] --> PLT[1] // 一一对应
GOT[4] --> PLT[2] // 相互协同,作用于一个函数
GOT[5] --> PLT[3] // 一个保存的是该函数所需要的延迟绑定的指令
GOT[6] --> PLT[4] // 一个是保存个该函数链接所需要的地址
. .
. .
. .
>>>> 一个段(节)三个节
一个段(节)三个节
1. .dynmic
// 该结构都有 64 位程序和 32 位程序的区别,不过大致结构相似,此处只讨论 64 位程序中的
// /usr/include/elf.h
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
// d_tag 识别该结构体表示的哪一个节,通过以此字段不同来寻找不同的节
union
{
Elf64_Xword d_val; /* Integer value */
// 对应节的地址,用于存储该结构体表示下的节所在的地址
Elf64_Addr d_ptr; /* Address value */
// 一般于上一个字段表示的值相同,所以笔者现在并不了解他们的区别
} d_un;
} Elf64_Dyn;
2. .dynsym
动态符号表,存储着在动态链接中所需要的每个函数所对应的符号信息,每个结构体分别对应一个符号 (函数) 。结构体数组。d_tag = DT_SYMTAB(值为 0x6) 的节。
(2)结构:
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
// 保存着该函数函数名在 .dynstr 中的偏移,可以结合 .dynstr 找到准确函数名。
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
// 如果这个符号被导出,则存有这个导出函数的虚拟地址,否则为NULL.
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
3. .dynstr
4. .rel.plt (.rela.plt)
(2)结构:
typedef struct
{
Elf64_Addr r_offset; /* Address */
// 此处表示的是解析完的函数真实地址存放的位置,
// 即对应解析函数的 GOT 表项地址
Elf64_Xword r_info; /* Relocation type and symbol index */
// 该结构主要用到高某位,表示索引,低位表示类型
// 例如:0x10000007 此处 1 表示索引,7 代表类型,主要用到 1 值,还记得上边在 PLT 中的指令嘛?
//每一个表项的第二条指令, PUSH 了一个索引,所 PUSH 的索引与此相关,
//也就是通过 PLT 中 PUSH 的索引找到当时解析的函数对应的此结构体的
} Elf64_Rel;
//与上一结构体类似,只是不同编译环境下产生的不同结构,作用相同,就不再次讨论
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
Elf32_Sword r_addend; /* Addend */
} Elf32_Rela;
>>>> 扩充结构体(在Full RELRO用到)
扩充结构体(在Full RELRO用到)
struct r_debug{ //由于并没有找到该结构体的定义,所以没有声明类型
r_version
r_map //指向 link_map
r_brk
r_state
r_ldbase
}
>>>> link map 结构
link map 结构
1. 简介
2.主要字段
l_next:链接着该程序所有用到的 libary
上边提到的 GOT[1] 中保存的地址是第一层 link_map 中所表示的 libary,此时是指向的程序本身,
不过可以用 l_next 结构寻找下一层表示的 libary,以此来遍历程序中所用到的 libary,
并利用下边所提到的字段找到该层 libary 的名字、基地址、以及所有的 section 等信息。
l_name:表示 libary 的名字
l_addr:表示 libary 的基地址
l_info[x]:指向该 libary 下的 .dynamic。
l_info[1] 指向 d_tag = 1 时所表示的 section ,所以可以改变 x 的值找到每个相关 section 的地址。
在链接过程中 binary 中的 section 地址,以及 libary 中的地址都是通过此方法确定的。
>>>> 概括描述
概括描述
// 上边的详细过程
reloc_arg --> 函数名 A
利用 link_map --> l_info[x] 通过改变 x 的值,确定 .dynsym .dynstr
再用 .dynsym 与 .dynstr 对整个动态符号表 .dynstym 进行遍历,去匹配函数名 A
若 某一个 Elf64_Sym(符号) 的 st_name + .dynstr == A
则 该 Elf64_Sym 表示的符号即为函数 A
// 整个过程可以这样理解,不过真实情况使用的 Hash 方法去寻找的这个 Elf64_Sym(符号)
>>>> 具体过程
具体过程
1. 调用某个函数后进入该函数的 PLT[x] ,在 PLT[x] 中 push 一个参数 reloc_arg 。
【答案 1】拿到这个 reloc_arg 后,链接器会通过该值找到对应函数的 Elf_Rel 结构,通过该结构的 r_info 变量中的偏移量找到对应函数的 Elf_Sym 结构,然后再通过 Elf_Sym 结构的 st_name 结合之前已经确定的 .dynstr 地址,通过 st_name + .dynstr 获得对应函数的函数名。
2. 在链接过程中 PLT[0] 会 push dl_runtime_resolve 函数的第二个参数 link_map。
【答案 2】拿到这个变量后链接器会获得所要解析的函数的函数库(通过 link_map 的 l_next 字段),然后拿到这个外部库之后 link_map 的 l_addr 字段会记录该库的基地址,然后链接器通过 new_hash 函数求出要链接函数的 hash(new_hash(st_name + .dynstr)),然后通过该 hash 和之前的保存值进行匹配,如果匹配上就获得了该函数在外部库的 Elf64_Sym 结构,然后通过该结构的 st_value 获取该函数在外部库里面的偏移,最后通过 st_value + l_addr 获取该函数的真实地址,最后通过 Elf64_Rel 的 r_offset 定位该函数在 GOT 中对应的地址,然后将最后结果写入该地址中。(其中有通过这两个参数共同获得的东西,不过为了便于理解就不再分开讨论。)
>>>> 保护手段(RELRO)
保护手段(RELRO)
1.无保护
2.部分保护
3.完全保护
>>>> 对应攻击方法
对应攻击方法
1. 无保护
过程
动态装载器是从 .dynamic 段的 DT_STRTAB 条目中获得 .dynstr 段的地址的,而且 DT_STRTAB 条目的位置是已知的,默认情况下也可写。我们可以将这个条目的 d_val 域覆盖为 .bss 段。
限制
这种方式非常简单,但仅当二进制程序的 .dynamic 段可写时有效。对于使用部分或完全 RELRO 编译的二进制程序,需要使用更复杂的攻击。
2. 部分保护
_dl_runtime_resolve 函数的第二个参数是 Elf_Rel 条目在 .rel.plt 段中对应当前请求函数的偏移。动态装载器将这个值加上 .rel.plt 的基地址来得到目标 Elf_Rel 结构的绝对地址。
过程
计算一个新的 reloc_arg 参数,将 _dl_runtime_resolve 解析的位置劫持到一个可控内存。然后在那里构造一个 Elf_Rel 结构,并填写 r_offset 的值为一个可写的内存地址,将最后解析出的函数地址写在那里。
简而言之,该过程伪造了函数链接中所需要的所有结构(Elf_Sym Elf_Rel .dynstr),通过控制 reloc_arg 指向到伪造的 Elf_Rel ,再通过Elf_Rel 中的 r_info 找到伪造的 Elf_Sym 最后通过 Elf_Sym 的 st_name 找到最终伪造后需要解析的函数(例:system),解析完后通过 Elf_Rel 的 r_offset 写回到正确位置,达到劫持函数解析的目的,最终执行自己想要执行的函数。
限制
首先,Elf_Rel 的下标需要是正数,因为 r_info 域在 ELF 标准中规定是一个无符号整数。这就意味着在实际中这块可写的内存空间(例如.bss段)必须是位于 .dynsym 段之后。
扩充方法
可以通过修改指向程序那一层的 link_map,具体做法是把该层 link_map->l_info[DT_STRTAB]->st_value 的值劫持到一个我们可控的区域,然后在该区域填充伪造函数,其实该方法也是通过修改 .dynstr 的方式实现攻击的手法。不过该方法必须有能够改写 st_value 值所需要的 gadget。
3. 完全保护
DT_DEBUG 条目的值是动态装载器在加载时设置好的,它指向一个 r_debug 类型的数据结构。
过程
攻击者使用 DT_DEBUG 这个动态条目来获取 r_debug 结构。接着,解引用 r_map 域从而得到主程序的 link_map 结构。然后像上边扩充方法那样破坏 l_info[DT_STRTAB]。
:) bilibili 视频(https://www.bilibili.com/video/av17482224)
看雪ID:1Oin0
https://bbs.pediy.com/user-864295.htm
推荐文章++++
好书推荐